Skip to content

feat: add library variable set list and view commands - #697

Draft
NickJosevski wants to merge 7 commits into
mainfrom
nj/issue-346
Draft

NickJosevski wants to merge 7 commits into
mainfrom
nj/issue-346

Conversation

@NickJosevski

@NickJosevski NickJosevski commented Aug 19, 2026 •

Copy link
Copy Markdown
Contributor

Refs #346

What this adds

A new top-level octopus library-variable-set command (aliases library-variable-sets, lvs) with two read-only subcommands.

library-variable-set list — lists the variable sets in the space, sorted by name, with --filter/-q for substring matching. Script modules share the libraryvariablesets endpoint and are excluded (the existing sharedVariable.GetAllLibraryVariableSets helper already filters on ContentType == "Variables").

library-variable-set view [<name> | <id>] — this is the part aimed at the issue. It resolves the set, fetches its variables, and groups every value stored under the same variable name into one entry, with the unscoped value first because it is the fallback the scoped ones override. --filter/-q narrows to variables whose name contains a string; --web/-w opens the set in the browser. Prompts for the set in interactive mode, errors in --no-prompt.

This addresses both pain points named in the issue:

  • "scoped and non-scoped versions show up as separate entries" — they are now one entry with several values.
  • "switching between /libraryvariablesets/ and /variables/ doesn't make much sense" — one command does both requests and stitches the result together.

Scope IDs are resolved to display names via the variable set's ScopeValues, falling back to the raw ID rather than erroring, so one unresolvable scope value can't break the whole view (this differs deliberately from pkg/cmd/project/variables/shared.ToScopeValues, which returns an error).

Table output (-f table, the default) prints one row per stored value, with the variable name on every row:

NAME         VALUE            SCOPE                    ID
Slack.Token  ***              (unscoped)               Variables-4
Slack.Url    https://default  (unscoped)               Variables-1
Slack.Url    https://prod     Environment: Production  Variables-2
Slack.Url    https://test     Environment: Test        Variables-3

-f json emits the set's own fields plus Variables as an array of {Name, Values[]}, each value carrying Id, Value, IsSensitive, IsScoped, a Scope object using the API's own property names (Environment, Role, Machine, TenantTag, …) with resolved names, and a flat ScopeSummary string. -f basic prints a grouped text block. Sensitive values are masked as ***.

This also lights up the previously-unused annotations.IsLibrary group, so the root help now has a LIBRARY COMMANDS section.

Deliberately left out

  • No create / update / delete, of sets or of variables. Editing a variable through /api/{space}/variables/variableset-… is a read-modify-write of the whole variable set with optimistic concurrency on Version; doing it safely (and prompting for scopes) is a much bigger surface than a first slice should carry, and it deserves its own design discussion — see the open question below.
  • Script modules — filtered out of list, and view won't resolve one.
  • Variable set templates (the tenant-facing variable templates on a set) — view reports only TemplateCount.
  • No new selector in pkg/question/selectors — resolution lives in pkg/cmd/libraryvariableset/shared, mirroring pkg/cmd/channel/shared.ResolveChannel. Worth promoting if a second command needs it.

Follow-up surface, roughly in the order I'd do it: create/delete of a set (cheap, the SDK has Add/Update/DeleteByID); variable create/update/delete within a set, reusing pkg/cmd/project/variables/shared scope flags; script-module support; templates.

Test evidence

go build ./... — clean.

go test ./pkg/... — pass, exit 0, no failures. New tests:

  • pkg/cmd/libraryvariableset/shared/shared_test.go — grouping (including case-insensitive names and unscoped-first ordering), scope ID→name resolution, fallback to the raw ID for unknown scope values, tolerance of a missing ScopeValues lookup, sensitive masking.
  • pkg/cmd/libraryvariableset/list/list_test.go — table/json/basic, name filter, script modules excluded.
  • pkg/cmd/libraryvariableset/view/view_test.go — required-in-automation-mode error, unknown-set error, interactive prompt, grouped table, basic, json.

Test support changes: NewLibraryVariableSet / NewVariableSetForLibraryVariableSet fixtures, and the LibraryVariables + Variables links added to testutil.NewRootResource() (they were missing, so the SDK couldn't build those service paths under test).

Decisions — settled on existing convention

  • Command noun: library-variable-set (alias lvs). Every multi-word command in the CLI is a flat kebab-case noun — project-group, worker-pool, ephemeral-environment, build-information — and there is no namespace-parent command anywhere in NewCmdRoot. variable-set would be ambiguous with project variable sets and octopus project variable; a library parent would commit us to a namespace none of whose siblings (library certificate, library script-module, library step-template) exist yet. Abbreviated aliases have precedent: kubernetes carries k8s. If we later want a library parent we can add it and alias.
  • JSON carries resolved names, not scope IDs. project variables list is the closest prior art: VariableAsJson embeds the SDK variable and shadows its ID-bearing Scope with a name-bearing VariableScopeValues, so consumers get names only. tenant variables list does the same (OwnerName, Environment as names). No ScopeIds field.
  • No scope filters on view. The seven --*-scope flags in pkg/cmd/project/variables/shared exist on the write commands only; project variables list takes just --project and --git-ref. Read commands in this CLI filter on name, and -f json is the answer for anything more selective.
  • The table repeats the variable name on every row, with an ID column. project variables list prints one row per variable with a bold NAME and a dimmed ID column, and no cell in any table in the CLI is blanked to imply "same as above". Blanking reads slightly better for a human, but it is a format nothing else in the CLI uses.

Open question — for a maintainer, not prior art

Should variable editing live in the CLI at all? This PR is read-only, which already solves the issue's stated problem (understanding a variable set), but the issue author links the "update a variable set variable" docs page, so they clearly want to write too.

The write path is a whole-variable-set read-modify-write with a Version for optimistic concurrency; a CLI that silently clobbers a concurrent edit would be worse than no CLI. It also needs sensitive-value handling — the server never returns them, so a naive round-trip would blank them.

My suggestion is a middle ground next: set-value for an existing variable identified by name + scope, a targeted read-modify-write with explicit conflict detection on Version, before attempting general create/delete. I'd like a call on whether we're comfortable with the CLI writing variable sets at all before building it.

🤖 Generated with Claude Code

@NickJosevski NickJosevski left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One correctness issue found (verified against the portal's route definitions); everything else looked good.

Comment thread pkg/cmd/libraryvariableset/view/view.go Outdated
}
}

webUrl := util.GenerateWebURL(opts.Host, set.SpaceID, fmt.Sprintf("library/variablesets/%s", set.GetID()))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The portal has no library/variablesets/{id} route, so this URL (printed in basic output, returned as WebUrl in JSON, and opened by --web) lands on the portal's not-found page. The variable set detail page lives at library/variables/{id}.

I verified this against the portal bundle of a current server: variableSetPage is defined with the route template /variables/{variableSetId} nested under /library, the legacy v3 redirects also use library/variables/..., and the string variablesets does not appear in any route template.

Suggested change
webUrl := util.GenerateWebURL(opts.Host, set.SpaceID, fmt.Sprintf("library/variablesets/%s", set.GetID()))
webUrl := util.GenerateWebURL(opts.Host, set.SpaceID, fmt.Sprintf("library/variables/%s", set.GetID()))

The expected URLs in view_test.go (the basic and json cases) and the WebUrl assertion in test/integration/libraryvariableset_test.go encode the same wrong path and will need updating with it.

@NickJosevski NickJosevski Sep 14, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actioned in cf36ff8.

viewRun now builds the link as fmt.Sprintf("library/variables/%s", set.GetID()), so the basic-output line, the WebUrl field in JSON, and --web all point at the real variable set page. The same commit updates the three places that encoded the old path: the basic expectation and the WebUrl in the json expectation in pkg/cmd/libraryvariableset/view/view_test.go (TestLibraryVariableSetView, cases "outputFormat basic" and "outputFormat json"), and the assert.Contains on result.WebUrl in testLibraryVariableSetView in test/integration/libraryvariableset_test.go.

Kept the path inline rather than pulling out a constant: buildinformation/view/view.go is the only sibling that builds a library/... deep link and it does the same inline fmt.Sprintf, so a shared helper would be a one-caller abstraction.

I grepped the repo for variablesets afterwards. The remaining hits are all the API endpoint libraryvariablesets (the /api/Spaces-1/libraryvariablesets/all request expectations in the list/view tests, the root-resource link in test/testutil/fakeoctopusserver.go, and two comments about script modules sharing that endpoint) - those are correct as the API route and unrelated to the portal route.

go build ./... and go test ./... pass except test/integration, which fails to launch without OCTOPUS_TEST_URL/OCTOPUS_TEST_APIKEY; the integration assertion change is therefore unverified by a run here.

One thing I did not re-verify myself: the portal route claim. I have no portal bundle or live server in this environment, so I took the /library/variables/{variableSetId} route template from your evidence rather than confirming it independently.

NickJosevski and others added 5 commits September 15, 2026 16:55
Adds `octopus library-variable-set` with `list` and `view`. `view` fetches
the set and its variables in one command and groups values that share a
name, so a variable with several scoped values reads as one entry instead
of several, in table, basic and json output.

Read-only for now; create/update/delete of sets and variables is not
included.

Refs #346

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
list -f json emitted ID/VariableSetID; every other list command and
view -f json use Id/VariableSetId.

view -f json emitted "Variables": null when --filter matched nothing,
and [] otherwise.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Covers what MockHttpServer can't: real ScopeValues resolution, the
server returning no value for a sensitive variable, script module
exclusion, and the two json fixes above.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The portal has no `library/variablesets/{id}` route; the variable set
detail page lives at `library/variables/{id}`. Verified against a current
server's portal bundle: the route segment is
`library:createRouteSegment(routeTemplate`/library`, { ...
variableSetPage: createPageRoute(routeTemplate`/variables/{variableSetId}`) })`,
the legacy v3 redirect for a variable set is `/library/variables/new`, and
the deep-link map sends `libraryvariablesets` ids to `variableSetPage`.
The string `variablesets` appears in no route template.

Updates the basic/json expectations in view_test.go and the WebUrl
assertion in the integration test with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
formatForBasic returned early on an empty group list, so `-f basic`
dropped the "View this library variable set in Octopus Deploy" line for a
set with no variables (or when --filter matched none) while every other
basic view in the CLI always prints it. Adds a test for the filtered case.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
NickJosevski and others added 2 commits September 17, 2026 18:13
project variables list prints a bold NAME on every row alongside a dimmed
ID column, and no table in the CLI blanks a cell to mean "same as above".
Blanking read slightly better for a human but is a format nothing else
uses, and it makes rows useless to grep and awk.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follows the table change; the integration assertions still expected the
blanked continuation cells.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant